# -*- coding: utf-8 -*-
"""
轮动策略框架 | 邢不行 | 2024分享会
author: 邢不行
微信: xbx6660
"""
import pandas as pd
import numpy as np
import itertools
from glob import glob
import os
from plotly.offline import plot
from plotly.subplots import make_subplots
import plotly.graph_objs as go
import ast
import time
from datetime import datetime


# ===读取各个资金曲线数据
def read_equity(path, strategy_dict, hold_period, offset, benchmark):
    """
    读取资金曲线数据
    :param path: 资金曲线的路径
    :param strategy_dict: 策略文件加下配置的子策略及其资金曲线的字典
    :param hold_period: 持仓周期
    :param offset: offset偏移
    :param benchmark: 基准数据
    :return:
        df，返回整理好的各个资金曲线的数据
    """
    base_name = os.path.basename(path)  # 取出路径的基础文件名，即资金曲线的文件名
    equity_name = base_name.split('_')[0]  # 解析资金曲线文件名，是由哪个策略跑出的资金曲线
    save_equity_cols = strategy_dict[f'Strategy_{equity_name}']  # 根据 资金曲线文件名 取出对应的参与轮动的资金曲线
    save_equity_cols_byclose = [col + '_byclose' for col in save_equity_cols]  # 根据指定的资金曲线取出对应的以收盘价计算的资金曲线

    # 读取equity文件
    equity = pd.read_csv(path, encoding='gbk', parse_dates=['candle_begin_time'])
    if len(equity) == 0:
        print('资金曲线为空，请检查资金曲线文件')
        exit()

    not_exist_cols = [col for col in save_equity_cols if col not in equity.columns]
    if not_exist_cols:
        print(not_exist_cols, '列不存在，请检查策略配置。')
        exit()
    equity = equity[['candle_begin_time', '选币'] + save_equity_cols + save_equity_cols_byclose]  # 保留部分列

    # 修改选币列的格式，生成三列，分别时多头选币、空头选币、多空选币
    equity['多空资金曲线_选币'] = equity['选币'].apply(lambda x: [symbol.strip() for symbol in x.split(' ')][: -1])  # 如果当周期为空仓，则选币为空[]
    equity['多头资金曲线_选币'] = equity['多空资金曲线_选币'].apply(lambda x: [symbol for symbol in x if symbol.endswith(',1)')])
    equity['空头资金曲线_选币'] = equity['多空资金曲线_选币'].apply(lambda x: [symbol for symbol in x if symbol.endswith(',-1)')])

    # ===resample至日线数据
    # 如果不是1H持仓周期的，存在不同的open、high、low、close；如果是1H持仓周期，则open=high=low=close
    resample_cols = {'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last', '选币': 'last'}

    # 创建空列表用来保存整理好的df
    equity_list = []
    for equity_col in save_equity_cols:  # 遍历指定的资金曲线
        # 取出对应的资金曲线数据、对应的选币数据、对应的以close计算的资金曲线数据
        _equity = equity[['candle_begin_time', equity_col, equity_col + '_byclose', equity_col + '_选币']]
        _equity.rename(columns={equity_col + '_选币': '选币'}, inplace=True)

        # 开始时间
        _start_date = _equity['candle_begin_time'].iloc[0]

        # 把资金曲线列复制成open、high、low、close。为了回测与实盘对应，使用close计算得到的资金曲线为基础，生成ohlc，计算因子
        _equity['open'] = _equity[equity_col + '_byclose']
        _equity['high'] = _equity[equity_col + '_byclose']
        _equity['low'] = _equity[equity_col + '_byclose']
        _equity['close'] = _equity[equity_col + '_byclose']

        # 计算每小时的涨跌幅并resample为list格式，计算涨跌幅仍然使用的是avg_price计算得到的资金曲线
        _equity['涨跌幅'] = _equity[equity_col].pct_change()
        _equity['涨跌幅'].fillna(_equity[equity_col] - 1, inplace=True)

        # ===与benchmark合并
        _equity = pd.merge(left=benchmark, right=_equity, on='candle_begin_time', how='left')
        _equity.sort_values(by='candle_begin_time', inplace=True)

        # resample数据
        period_equity = _equity.resample(hold_period, on='candle_begin_time', base=offset).agg(resample_cols)
        period_equity.reset_index(inplace=True)

        # resample涨跌幅为每小时涨跌幅
        period_equity['每小时涨跌幅'] = _equity.resample(hold_period, on='candle_begin_time', base=offset)['涨跌幅'].apply(lambda x: list(x)).values

        # 根据开始时间截取之后的数据
        period_equity = period_equity[period_equity['candle_begin_time'] >= _start_date]

        # 指定策略名；格式为 策略名 + 多空/多头/空头
        period_equity['symbol'] = equity_name + '_' + equity_col.split('资金曲线')[0]

        # =把生成好的df添加到列表中
        equity_list.append(period_equity)

    # 合并并整理数据
    equity = pd.concat(equity_list, axis=0)
    equity = equity.sort_values(['candle_begin_time', 'symbol']).reset_index(drop=True)

    return equity


def calc_factors_for_filename(df, factor_list, filename='', param_list=[]):
    """
    使用文件夹下的因子脚本进行计算因子
    :param df: 原始k线数据
    :param factor_list: 需要计算的因子列表
    :param filename: 指定因子文件夹名称
    :param param_list: 因子参数
    :return:
    """
    column_list = []
    # 根据config中设置的因子列表，逐个计算每个因子的数据
    for factor in factor_list:
        factor = factor.split('.')[0]
        _cls = __import__('%s.%s' % (filename, factor), fromlist=('',))
        # 兼容get_parameter
        if 'get_parameter' in _cls.__dict__:  # 如果存在get_parameter，直接覆盖config中配置的factor_param_list
            _param_list = getattr(_cls, 'get_parameter')()
        else:  # 如果不存在get_parameter，直接用config中配置的factor_param_list
            _param_list = param_list.copy()
        # 遍历参数，计算每个参数对应的因子值
        for n in _param_list:
            # 构建因子列名
            factor_name = f'{factor}_{str(n)}'
            # 因子已经计算过，则跳过
            if factor_name in df.columns:
                continue
            # 计算因子
            df = getattr(_cls, 'signal')(df, n, factor_name)
            # 为了跟实盘保持一致，所有因子信息在下个周期生效
            df[factor_name] = df[factor_name].shift(1)
            # 保存因子列名
            column_list.append(factor_name)

    return df, column_list


def calc_factors(all_equity, symbol, factor_names, factor_params):
    """
    计算因子
    :param all_equity: 所有资金曲线的数据
    :param symbol: 资金曲线名
    :param factor_names: 计算的因子
    :param factor_params: 计算因子的参数
    :return:
        返回每个资金曲线计算好因子之后的df
    """
    # 取出对应资金曲线的数据
    df = all_equity[all_equity['symbol'] == symbol].reset_index(drop=True)
    df = df.dropna(subset=['open', 'high', 'low', 'close', '选币']).reset_index(drop=True)

    # 计算涨跌幅
    df['涨跌幅'] = df['close'].pct_change()
    df['涨跌幅'].fillna(df['close'] / df['open'] - 1, inplace=True)

    # =根据净值数据计算因子
    df, _ = calc_factors_for_filename(df, factor_names, filename='factors', param_list=factor_params)

    return df


def save_inter_data(df, factor_cols, save_path):
    df = df[['candle_begin_time', 'symbol', '选币', '每小时涨跌幅'] + factor_cols]
    df.to_pickle(save_path)


def select_strategy(df, factor_list, select_num, **kwargs):
    """
    根据计算出的因子选择策略
    :param df: 所有资金曲线的数据，已经计算好因子
    :param factor_list: 因子列表
    :param select_num: 选策略的数量
    :param kwargs:
    :return:
        返回根据因子选择出来的策略
    """

    # 接受外部回测的因子列表，这里主要是去适配`4_遍历选币参数.py`
    external_list = kwargs.get('external_list', [])
    if external_list:  # 如果存在外部回测因子列表，则使用外部因子列表
        _factor_list = external_list
    else:  # 如过不存在外部回测因子列表，默认使用当前策略的因子列表
        _factor_list = factor_list

    # 同保温杯框架，计算得到因子排名值
    df['_rank'] = 0
    for factor_name, if_reverse, parameter_list, weight in _factor_list:
        col_name = f'{factor_name}_{str(parameter_list)}'
        # 计算单个因子的排名
        df[col_name + '_rank'] = df.groupby('candle_begin_time')[col_name].rank(ascending=if_reverse, method='min')
        # 将因子按照权重累加
        df['_rank'] += (df[col_name + '_rank'] * weight)
    df['rank'] = df.groupby('candle_begin_time')['_rank'].rank(ascending=True, method='first')

    # 选择排名小于select_num的对应的策略
    df = df[df['rank'] <= select_num].reset_index(drop=True)
    if df.empty:
        print('没有选出轮动的资金曲线，请检查配置')
        exit()

    return df


def cal_rot_net_value(df, hold_period, spot_buy_crate, spot_sell_crate, swap_buy_crate, swap_sell_crate):
    """
        根据轮动出来的资金曲线，计算轮动的资金曲线
    :param df: 选择策略之后的df
    :param hold_period: 持仓周期
    :param spot_buy_crate: 现货买入手续费
    :param spot_sell_crate: 现货卖出手续费
    :param swap_buy_crate: 合约买入手续费
    :param swap_sell_crate: 合约卖出手续费
    :return:
        返回计算出来的轮动资金曲线
    """
    # 根据选币结果得到选币数量
    df['选币'].fillna('空仓', inplace=True)
    df['选币'] = df['选币'].apply(lambda x: [symbol.strip() for symbol in x.split(' ')][: -1] if x == '空仓' else x)
    df['选币数量'] = df['选币'].apply(len)

    # 整理每周期的数据
    df['symbol'] += ' '
    group = df.groupby('candle_begin_time')
    select_stg = pd.DataFrame()
    select_stg['策略名称'] = group['symbol'].sum()
    select_stg['选币'] = group['选币'].sum()
    select_stg['策略数量'] = group['symbol'].size()
    select_stg['选币数量'] = group['选币数量'].apply(lambda x: list(x)).values

    # 计算每小时资金曲线
    select_stg['每小时资金曲线'] = group['每小时涨跌幅'].apply(lambda x: np.cumprod(np.array(list(x)) + 1, axis=1).mean(axis=0))
    # 计算每小时涨跌幅
    select_stg['每小时涨跌幅'] = select_stg['每小时资金曲线'].apply(lambda x: list(np.array(x) / np.array([1.0] + list(x[:-1])) - 1))
    del select_stg['每小时资金曲线']
    select_stg.reset_index(inplace=True)

    # =====计算开平仓比例
    # ===生成每个周期每个币种的资金占比
    # 遍历选币数量，生成每个策略中每个币种的资金占比
    _select_num = select_stg['策略数量'].mode()[0]
    for num in range(_select_num):
        if num == 0:
            select_stg['占比'] = select_stg['选币数量'].apply(lambda x: (x[num] * [1 / _select_num / x[num]]) if x[num] != 0 else [0])
        else:
            select_stg['占比'] += select_stg['选币数量'].apply(lambda x: (x[num] * [1 / _select_num / x[num]]) if x[num] != 0 else [])

    def get_ratios(df, date_range):
        """
            计算每个币种的开平仓比例
        :param df: 每个币种每个周期的资金占比数据
        :return:
            每个币种每个时间的资金占比、及开平仓比例
        """
        df = pd.merge(date_range, df, on='candle_begin_time', how='left')
        df['上周期占比'] = df['占比'].shift()
        df['下周期占比'] = df['占比'].shift(-1)
        df.dropna(subset=['选币'], axis=0, inplace=True)

        df['上周期占比'].fillna(0, inplace=True)
        df['下周期占比'].fillna(0, inplace=True)

        df['开仓比例'] = df['占比'] - df['上周期占比']
        df['开仓比例'] = np.where(df['开仓比例'] < 0, 0, df['开仓比例'])

        df['平仓比例'] = df['占比'] - df['下周期占比']
        df['平仓比例'] = np.where(df['平仓比例'] < 0, 0, df['平仓比例'])

        return df

    # ===根据每周期资金占比得到开平仓比例
    # 取出时间、选币两列，拆分选币。每个时间周期都会对应多个选币
    coins = select_stg[['candle_begin_time', '选币']].explode('选币')
    coins['选币'].fillna('', inplace=True)  # 如果该时间周期没有选币，则explode之后会显示为nan值。这边将nan值填充为 空字符串
    coins['占比'] = select_stg['占比'].sum()  # 将资金占比添加到对应的选币中
    coins = coins.groupby(['candle_begin_time', '选币'])['占比'].sum().reset_index()  # 如果选择多个策略，则每个周期的选币可能重复，将重复币种的资金占比加到一起

    date_range = pd.DataFrame(pd.date_range(start=coins['candle_begin_time'].min(), end=coins['candle_begin_time'].max(), freq=hold_period))
    date_range.rename(columns={0: 'candle_begin_time'}, inplace=True)

    coins = coins.groupby('选币').apply(get_ratios, date_range)  # 根据每个币种计算开仓比例和平仓比例
    coins.reset_index(drop=True, inplace=True)
    coins = coins.sort_values(['candle_begin_time', '选币']).reset_index(drop=True)  # 整理数据
    coins['symbol_type'] = coins['选币'].apply(lambda x: x.split(',')[0].split('(')[1] if x != '' else '')  # 从选币中取出币种类型，之后计算手续费 现货和合约 分开计算

    # 通过开平仓比例及币种类型计算合约开仓比例、现货开仓比例
    select_stg['开仓比例'] = coins.groupby('candle_begin_time')['开仓比例'].sum().values
    select_stg['合约开仓比例'] = coins.groupby('candle_begin_time').apply(lambda x: x[x['symbol_type'] == 'swap']['开仓比例'].sum()).values
    select_stg['现货开仓比例'] = coins.groupby('candle_begin_time').apply(lambda x: x[x['symbol_type'] == 'spot']['开仓比例'].sum()).values
    select_stg['平仓比例'] = coins.groupby('candle_begin_time')['平仓比例'].sum().values
    select_stg['合约平仓比例'] = coins.groupby('candle_begin_time').apply(lambda x: x[x['symbol_type'] == 'swap']['平仓比例'].sum()).values
    select_stg['现货平仓比例'] = coins.groupby('candle_begin_time').apply(lambda x: x[x['symbol_type'] == 'spot']['平仓比例'].sum()).values

    # 判断是否换策略，将没有换策略的周期的开平仓比例设为0(已经在子策略资金曲线中计算过一次)
    select_stg['开仓'] = np.where(select_stg['策略名称'] != select_stg['策略名称'].shift(), 1, 0)
    select_stg['平仓'] = np.where(select_stg['策略名称'] != select_stg['策略名称'].shift(-1), 1, 0)
    select_stg.loc[select_stg['开仓'] == 0, ['开仓比例', '合约开仓比例', '现货开仓比例']] = 0
    select_stg.loc[select_stg['平仓'] == 0, ['平仓比例', '合约平仓比例', '现货平仓比例']] = 0

    # 计算手续费
    def cal_exchange_fee(row):
        """
        在资金曲线上加上手续费的计算
        每个点应扣的手续费 =  ∑（手续费率 x 仓位价值）
        :param row: 每一行的数据
        :return:每个点应扣的手续费
        """
        pos_value = np.array(row['仓位价值'])
        # 开仓的手续费 = lvg * rate * 开仓调整比例
        open_fee = row['合约开仓比例'] * swap_buy_crate + row['现货开仓比例'] * spot_buy_crate
        # 平仓的手续费 = 仓位价值 * rate * 平仓调整比例
        close_fee = row['仓位价值'][-1] * (row['合约平仓比例'] * swap_sell_crate + row['现货平仓比例'] * spot_sell_crate)

        # 小时级别的持仓长度（1H持仓长度为1，3H持仓长度为3）
        length = len(pos_value)
        if length > 1:
            # 如果周期长度大于2，每个点的手续费为：[开仓手续费，0,0,0,0，……，平仓手续费]（中间的0也可以是0个0）
            exchange_fee = [open_fee] + [0] * (length - 2) + [close_fee]
        else:
            # 如果周期长度等于1，即这根K线又要开仓又要平仓，手续费 = 开仓手续费 + 平仓手续费
            exchange_fee = [open_fee + close_fee]
        # 对手续费求和
        exchange_fee = np.cumsum(np.array(exchange_fee))

        return exchange_fee

    # 通过开平仓比例、手续费率、合约占比计算手续费
    select_stg['仓位价值'] = select_stg.apply(lambda row: np.cumprod(np.array(row['每小时涨跌幅']) + 1), axis=1)
    select_stg['每小时手续费'] = select_stg.apply(lambda rows: cal_exchange_fee(rows), axis=1)

    # 计算每小时资金曲线
    select_stg['每小时资金曲线'] = select_stg.apply(lambda rows: rows['仓位价值'] - rows['每小时手续费'], axis=1)
    # 计算每小时涨跌幅
    select_stg['每小时涨跌幅'] = select_stg['每小时资金曲线'].apply(lambda x: list(np.array(x) / np.array([1.0] + list(x[:-1])) - 1))
    # 计算周期的涨跌幅
    select_stg['周期涨跌幅'] = select_stg['每小时资金曲线'].apply(lambda x: x[-1] - 1)

    # 使用label数据为之后的统计、分段画图做准备
    select_stg['label'] = select_stg['开仓'].expanding().sum()

    # 整理数据
    select_stg = select_stg[['candle_begin_time', '策略名称', '选币', '策略数量', '周期涨跌幅', '每小时涨跌幅', '每小时资金曲线', 'label']]

    return select_stg


def get_benchmark(start_date, end_date, freq):
    benchmark = pd.DataFrame(pd.date_range(start=start_date, end=end_date, freq=freq))
    benchmark.rename(columns={0: 'candle_begin_time'}, inplace=True)

    return benchmark


def revise_data_length(data, data_len):
    """
    校正数据长度
    原数据过长，则进行切片
    原数据果断，则使用0填充
    :param data: 原数据
    :param data_len: 资金曲线的数据长度
    :return: 校正后的数据
    """
    if len(data) > data_len:
        data = data[0:data_len]
    elif len(data) < data_len:
        data = data.append(pd.Series([0] * (data_len - len(data))))

    return data


# 计算策略评价指标
def strategy_evaluate(equity, net_col='轮动资金曲线', pct_col='轮动涨跌幅'):
    """
    回测评价函数
    :param equity: 资金曲线数据
    :param net_col: 资金曲线列名
    :param pct_col: 周期涨跌幅列名
    :return:
    """
    # ===新建一个dataframe保存回测指标
    results = pd.DataFrame()

    # 将数字转为百分数
    def num_to_pct(value):
        return '%.2f%%' % (value * 100)

    # ===计算累积净值
    results.loc[0, '累积净值'] = round(equity[net_col].iloc[-1], 2)

    # ===计算年化收益
    annual_return = (equity[net_col].iloc[-1]) ** (
            '1 days 00:00:00' / (equity['candle_begin_time'].iloc[-1] - equity['candle_begin_time'].iloc[0]) * 365) - 1
    results.loc[0, '年化收益'] = num_to_pct(annual_return)

    # ===计算最大回撤，最大回撤的含义：《如何通过3行代码计算最大回撤》https://mp.weixin.qq.com/s/Dwt4lkKR_PEnWRprLlvPVw
    # 计算当日之前的资金曲线的最高点
    equity['max2here'] = equity[net_col].expanding().max()
    # 计算到历史最高值到当日的跌幅，drowdwon
    equity['dd2here'] = equity[net_col] / equity['max2here'] - 1
    # 计算最大回撤，以及最大回撤结束时间
    end_date, max_draw_down = tuple(equity.sort_values(by=['dd2here']).iloc[0][['candle_begin_time', 'dd2here']])
    # 计算最大回撤开始时间
    start_date = equity[equity['candle_begin_time'] <= end_date].sort_values(by=net_col, ascending=False).iloc[0]['candle_begin_time']
    # 将无关的变量删除
    # temp.drop(['max2here', 'dd2here'], axis=1, inplace=True)
    results.loc[0, '最大回撤'] = num_to_pct(max_draw_down)
    results.loc[0, '最大回撤开始时间'] = str(start_date)
    results.loc[0, '最大回撤结束时间'] = str(end_date)
    # ===年化收益/回撤比：我个人比较关注的一个指标
    results.loc[0, '年化收益/回撤比'] = round(annual_return / abs(max_draw_down), 2)
    # ===统计每个周期
    results.loc[0, '盈利周期数'] = len(equity.loc[equity[pct_col] > 0])  # 盈利笔数
    results.loc[0, '亏损周期数'] = len(equity.loc[equity[pct_col] <= 0])  # 亏损笔数
    results.loc[0, '胜率'] = num_to_pct(results.loc[0, '盈利周期数'] / len(equity))  # 胜率
    results.loc[0, '每周期平均收益'] = num_to_pct(equity[pct_col].mean())  # 每笔交易平均盈亏
    results.loc[0, '盈亏收益比'] = round(equity.loc[equity[pct_col] > 0][pct_col].mean() / equity.loc[equity[pct_col] <= 0][pct_col].mean() * (-1), 2)  # 盈亏比
    results.loc[0, '单周期最大盈利'] = num_to_pct(equity[pct_col].max())  # 单笔最大盈利
    results.loc[0, '单周期大亏损'] = num_to_pct(equity[pct_col].min())  # 单笔最大亏损

    # ===连续盈利亏损
    results.loc[0, '最大连续盈利周期数'] = max(
        [len(list(v)) for k, v in itertools.groupby(np.where(equity[pct_col] > 0, 1, np.nan))])  # 最大连续盈利次数
    results.loc[0, '最大连续亏损周期数'] = max(
        [len(list(v)) for k, v in itertools.groupby(np.where(equity[pct_col] <= 0, 1, np.nan))])  # 最大连续亏损次数

    # ===其他评价指标
    results.loc[0, '收益率标准差'] = num_to_pct(equity[pct_col].std())

    # ===每年、每月收益率
    temp = equity.copy()
    temp.set_index('candle_begin_time', inplace=True)
    year_return = temp[[pct_col]].resample(rule='A').apply(lambda x: (1 + x).prod() - 1)
    month_return = temp[[pct_col]].resample(rule='M').apply(lambda x: (1 + x).prod() - 1)

    def num2pct(x):
        if str(x) != 'nan':
            return str(round(x * 100, 2)) + '%'
        else:
            return x

    year_return['涨跌幅'] = year_return[pct_col].apply(num2pct)

    # 对每月收益进行处理，做成二维表
    month_return.reset_index(inplace=True)
    month_return['year'] = month_return['candle_begin_time'].dt.year
    month_return['month'] = month_return['candle_begin_time'].dt.month
    month_return.set_index(['year', 'month'], inplace=True)
    del month_return['candle_begin_time']
    month_return_all = month_return[pct_col].unstack()
    month_return_all.loc['mean'] = month_return_all.mean(axis=0)
    month_return_all = month_return_all.apply(lambda x: x.apply(num2pct))

    return results.T, year_return[['涨跌幅']], month_return_all


def get_variable_from_py_file(py_path, var_dict):
    """
    从指定的py文件路径中获取指定的变量
    :param py_path: py文件名路径
    :param var_dict: 参数列表，{参数名:类型}
    :return:
        {'spot_c_rate': 0.0004, 'swap_c_rate': 0.001}
    """
    # 判断文件是否存在
    if os.path.exists(py_path):
        # 逐行读入文件
        with open(py_path, 'r', encoding='utf-8') as file:
            lines = file.readlines()
        # 创建新{}保存结果
        res = {}
        # ===将指定的变量和读取到的字符串一一对应
        for var in var_dict.keys():  # 遍历指定的需要获取参数变量
            for line in lines:  # 遍历读取到的各行字符串
                if line.startswith(var):  # 判断是否存在对应的变量
                    # 如果这行代码有注释，先注释之后的内容
                    if '#' in line:
                        inx = line.find('#')
                        line = line[:inx]
                    # 替换掉字符串中的空格及换行
                    line = line.replace('\n', '').replace(' ', '')
                    # 将字符串以=分隔为两份
                    sub_str = line.split('=')
                    # 如果指定的变量类型是str，读取到的是None，则会返回None
                    if var_dict[var] == str and sub_str[1].replace('\'', '').replace('\"', '') == 'None':
                        res[sub_str[0]] = None
                    elif var_dict[var] == bool:  # 指定的变量类型是bool
                        res[sub_str[0]] = eval(sub_str[1])
                    elif var_dict[var] == str:  # 指定的变量类型是str
                        res[sub_str[0]] = sub_str[1].replace('\'', '').replace('\"', '')
                    elif var_dict[var] == float and '/' in sub_str[1]:  # 如果指定的是float，但是读取到的是包含/的分数
                        res[sub_str[0]] = eval(sub_str[1])  # 计算float类型
                    else:  # 其余类型直接使用指定的类型
                        res[sub_str[0]] = var_dict[var](sub_str[1])
                    break
        return res
    else:  # 如果文件不存在则返回空{}
        print('路径错误，未找到对应的py文件：', py_path)
        return {}


def get_equity_path(base_strategy_dict, base_config_path):
    """
    获取对应策略中指定的hold_period、offset、所有对应的资金曲线文件
    :param base_strategy_dict:
    :param base_config_path:
    :return:
        hold_period、offset、资金曲线列表
    """
    # 获取指定策略文件的所有hold_period和offset
    hold_period_set, offset_set = set(), set()
    if_use_spots = []

    # 遍历每个策略文件名
    for stg_name in base_strategy_dict:
        stg_name += '.py'  # 在策略文件名后加上后缀  .py
        stg_path = os.path.join(base_config_path, 'program/strategy', stg_name)  # 拼接路径取出策略文件的路径

        # 获取策略文件名中的hold_period、offset变量
        stg_params = get_variable_from_py_file(stg_path, {'hold_period': eval, 'offset': int, 'if_use_spot': bool})
        if len(stg_params) == 0:
            print('请检查路径配置及对应的策略文件是否存在。')
            exit()
        _hold_period = stg_params['hold_period']  # 取出hold_period
        _offset = stg_params['offset']  # 取出offset
        _if_use_spot = stg_params['if_use_spot']
        if_use_spots.append(_if_use_spot)

        # 将该策略文件中的hold_period、offset添加到集合中
        hold_period_set.add(_hold_period)
        offset_set.add(_offset)

    # 如果各策略文件存在多个持仓周期或offset，则会退出
    if (len(hold_period_set) > 1) | (len(offset_set) > 1):
        print('存在多个持仓周期或offset，请调整一致。')
        exit()

    # 取出唯一的hold_period和offset
    hold_period = list(hold_period_set)[0]
    offset = list(offset_set)[0]

    # 资金曲线文件路径
    equity_path = os.path.join(base_config_path, 'data/回测结果/')
    # 获取所有的资金曲线文件，只找hold_period和offset对应的资金曲线文件
    all_equity_file = []  # 创建新列表保存资金曲线文件
    for i, stg in enumerate(list(base_strategy_dict.keys())):  # 遍历每个策略文件名
        if_use_spot = if_use_spots[i]
        if if_use_spot:
            label = 'SPOT'
        else:
            label = 'SWAP'

        file_name = f'{stg.split("_")[1]}_{label}_资金曲线_{hold_period}_{offset}.csv'  # 以策略文件名_后的部分开头，之后是SPOT或SWAP，再接着hold_period和offset
        equity_file = glob(equity_path + file_name)  # 将文件夹下所有对应的资金曲线文件找出来
        if len(equity_file) == 0:
            print('没有找到对应策略的资金曲线：', stg)
            print('请修改策略配置或生成对应的资金曲线')
            exit()
        all_equity_file.extend(equity_file)  # 添加到列表中

    return hold_period, offset, if_use_spots, all_equity_file


def display_stat_data(df, info_path, stg_name, hold_period, offset):
    """
        计算得到的资金曲线数据
    :param df:
    :param info_path: 数据保存路径
    :param stg_name: 策略文件名
    :param hold_period: 持仓周期
    :param offset: offset
    :return:
        打印并保存统计数据
    """
    # label代表第几次换仓，通过label取得是否换仓列
    df['是否换仓'] = df['label'].diff()
    df['是否换仓'].fillna(1, inplace=True)

    # ===统计各个策略的选中频率、胜率、平均涨幅
    group = df.groupby('策略名称')
    select_stat = group.size().sort_values(ascending=False).to_frame().rename(columns={0: '选中周期数'})
    select_stat['选中周期比例'] = select_stat['选中周期数'] / select_stat['选中周期数'].sum()
    select_stat['选中次数'] = group.agg({'是否换仓': 'sum'})
    select_stat['选中次数比例'] = select_stat['选中次数'] / df['label'].max()
    select_stat['选中周期胜率'] = group.apply(lambda x: (x['周期涨跌幅'] > 0).sum() / len(x))
    select_stat['选中周期平均涨幅'] = group.apply(lambda x: x['周期涨跌幅'].mean())
    temp_df = df.groupby(['策略名称', 'label']).apply(lambda x: np.prod(x['周期涨跌幅'] + 1) - 1).to_frame()
    temp_df.reset_index(inplace=True)
    select_stat['选中次数胜率'] = temp_df.groupby('策略名称').apply(lambda x: (x[0] > 0).sum() / len(x))
    select_stat['选中次数平均涨幅'] = temp_df.groupby('策略名称').apply(lambda x: x[0].mean())
    df.drop('是否换仓', axis=1, inplace=True)

    # ===统计换仓次数
    times = df.resample('Y', on='candle_begin_time').apply(lambda x: x['label'].max() - x['label'].min())
    times = times.reset_index().rename(columns={'candle_begin_time': '年份', 0: '换仓次数'})
    times['年份'] = times['年份'].apply(lambda x: str(x).split('-')[0] + '年')
    times.loc[0, '换仓次数'] += 1
    times.set_index('年份', inplace=True)
    times.loc['全区间', '换仓次数'] = df['label'].max()

    # ===统计最大、最小换仓天数，分别有多少次、以及对应的时间
    df['candle_begin_time'] = df['candle_begin_time'].apply(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'))
    size = df.groupby('label').size()
    max_size = size.max()
    min_size = size.min()
    max_size_freq = len(size[size == max_size])
    min_size_freq = len(size[size == min_size])
    max_size_times, min_size_times = [], []
    for idx in size[size == max_size].index:
        temp_df = df[df['label'] == idx]
        max_size_times.append([temp_df['candle_begin_time'].min(), temp_df['candle_begin_time'].max()])
    for idx in size[size == min_size].index:
        temp_df = df[df['label'] == idx]
        min_size_times.append([temp_df['candle_begin_time'].min(), temp_df['candle_begin_time'].max()])
    df['candle_begin_time'] = pd.to_datetime(df['candle_begin_time'])

    # 打印统计信息
    print('=====子策略表现=====')
    print(select_stat)
    print('=====换仓次数=====')
    print('各年换仓次数：\n', times)
    print('=====最大/最小持仓周期数=====')
    print('最大持仓周期数', max_size)
    print('最小持仓周期数', min_size)
    print('=====出现最大/最小换仓周期数的次数')
    print('最大持仓周期数出现的次数', max_size_freq)
    print('最小持仓周期数出现的次数', min_size_freq)
    print('=====最大/最小换仓周期数对应开仓/平仓时间=====')
    print('最大持仓周期数出现的时间(开仓 平仓)', max_size_times)
    print('最小持仓周期数出现的时间(开仓 平仓)', min_size_times)

    # 保存统计信息
    save_path = os.path.join(info_path, f'{stg_name}_子策略表现_{hold_period}_{offset}.csv')
    select_stat.to_csv(save_path, encoding='gbk')
    save_path = os.path.join(info_path, f'{stg_name}_选中次数_{hold_period}_{offset}.csv')
    times.to_csv(save_path, encoding='gbk')


def get_equity_file(base_strategy_dict, base_config_path, hold_period, offset):
    """
    获取对应策略中指定的hold_period、offset、所有对应的资金曲线文件
    :param base_strategy_dict:
    :param base_config_path:
    :param hold_period:
    :param offset:
    :return:
        资金曲线列表
    """
    if_use_spots = []

    print(base_strategy_dict)
    # 遍历每个策略文件名
    for stg_name in base_strategy_dict:
        stg_name += '.py'  # 在策略文件名后加上后缀  .py
        stg_path = os.path.join(base_config_path, 'program/strategy', stg_name)  # 拼接路径取出策略文件的路径

        # 获取策略文件名中的hold_period、offset变量
        stg_params = get_variable_from_py_file(stg_path, {'if_use_spot': bool})
        _if_use_spot = stg_params['if_use_spot']
        if_use_spots.append(_if_use_spot)

    # 资金曲线文件路径
    equity_path = os.path.join(base_config_path, 'data/回测结果/')
    # 获取所有的资金曲线文件，只找hold_period和offset对应的资金曲线文件
    all_equity_file = []  # 创建新列表保存资金曲线文件
    for i, stg in enumerate(list(base_strategy_dict.keys())):  # 遍历每个策略文件名
        if_use_spot = if_use_spots[i]
        if if_use_spot:
            label = 'SPOT'
        else:
            label = 'SWAP'

        file_name = f'{stg.split("_")[1]}_{label}_资金曲线_{hold_period}_{offset}.csv'  # 以策略文件名_后的部分开头，之后是SPOT或SWAP，再接着hold_period和offset
        equity_file = glob(equity_path + file_name)  # 将文件夹下所有对应的资金曲线文件找出来
        if len(equity_file) == 0:
            print('没有找到对应策略的资金曲线：', stg)
            print('请修改策略配置或生成对应的资金曲线')
            exit()
        all_equity_file.extend(equity_file)  # 添加到列表中

    return all_equity_file


def net_fund(df):
    first_log_index = (df['type'] == 'log').idxmax()
    df = df.loc[first_log_index:, :]
    df.reset_index(inplace=True, drop=True)

    df.loc[0, '净值'] = 1
    df.loc[0, '份额'] = df.iloc[0]['账户总净值'] / df.iloc[0]['净值']
    df.loc[0, '当前总市值'] = df.iloc[0]['账户总净值']
    for i in range(1, len(df)):
        if df.iloc[i]['type'] == 'log':
            df.loc[i, '当前总市值'] = df.iloc[i]['账户总净值']
            df.loc[i, '份额'] = df.iloc[i - 1]['份额']
            df.loc[i, '净值'] = df.iloc[i]['当前总市值'] / df.loc[i]['份额']
        if df.iloc[i]['type'] == 'transfer':
            reduce_cnt = df.iloc[i]['账户总净值'] / df.iloc[i - 1]['净值']
            df.loc[i, '份额'] = df.loc[i - 1]['份额'] + reduce_cnt
            df.loc[i, '当前总市值'] = df.iloc[i]['账户总净值'] + df.iloc[i - 1]['当前总市值']
            df.loc[i, '净值'] = df.iloc[i]['当前总市值'] / df.iloc[i]['份额']

    return df


def deal_with_equity(equity_df, transfer_df):
    """
    画资金曲线
    :param equity_df:   资金曲线数据
    :param transfer_df: 划转数据
    """
    # =合并转换划转记录
    equity_df['type'] = 'log'
    equity_df = pd.concat([equity_df, transfer_df], ignore_index=True)
    equity_df.sort_values('time', inplace=True)
    equity_df.reset_index(inplace=True, drop=True)
    # =计算净值
    equity_df = net_fund(equity_df)
    equity_df = equity_df[equity_df['type'] == 'log']

    return equity_df


def select_coin_compare(test_select_coin, real_select_coin, offset, n, hold_period, param):
    """
    对比实盘回测选币
    :param test_select_coin: 回测选币路径
    :param real_select_coin: 实盘选币路径
    :param offset: 对比的offset
    :param n: 对比周期数
    :param hold_period: 持仓周期
    :param param: 参数
    :return:
        如果存在不一致的选币，给打印出对应信息
    """
    print('=' * 20, '选币对比', '=' * 20)

    # 读取回测选币路径
    if os.path.exists(test_select_coin):
        test_select_coin = pd.read_csv(test_select_coin, encoding='gbk', parse_dates=['candle_begin_time'])
    else:
        print(f'找不到回测选币文件，请检查文件及路径是否配置正确：{test_select_coin}')
        exit()

    # 整理出实盘选币的文件
    real_select_coin_paths = glob(real_select_coin + '*.pkl')
    if len(real_select_coin_paths) == 0:
        print(f'找不到实盘选币文件，请检查文件及路径是否配置正确：{real_select_coin}')
        exit()

    # 新建compare_time、compare_set，用来记录对比的周期
    compare_set = set()  # 已经对比过的所有时间及文件，包含没有数据的时间等
    compare_time = set()  # 实际对比过的时间，不包含没有数据的时间
    same_time = set()  # 相同选币的时间
    diff_time = set()  # 不同选币的时间

    # 对于实盘选币的文件，从最新的选币开始进行对比
    for real_path in sorted(real_select_coin_paths, reverse=True):
        # 如果已经对比过这个文件，则跳过
        if real_path in compare_set:
            continue

        # 读取实盘选币文件
        real_select_coin = pd.read_pickle(real_path)
        # 只筛选出指定offset和该param下的选币
        real_select_coin = real_select_coin[real_select_coin['offset'] == offset].reset_index(drop=True)
        real_select_coin = real_select_coin[real_select_coin['param'] == param].reset_index(drop=True)

        # 如果选币中不包含这个offset的选币，则跳过
        if real_select_coin.empty:
            print(f'实盘选币文件:{real_path}无该offset选币，跳过该时间。')
            compare_set.add(real_path)
            continue
        time_diff = pd.Timedelta(days=1) if hold_period[-1:] == 'D' else pd.Timedelta(hours=1)
        real_select_coin['candle_begin_time'] += time_diff  # 实盘和回测会相差一个小时，所以实盘需要对齐时间
        _time = real_select_coin['candle_begin_time'].iloc[0]  #e 选币的时间

        # 如果已经对比过这个时间点，则跳过
        if _time in compare_set:
            continue

        # 如果这个时间点超过了回测的最新时间，则跳过
        if _time > test_select_coin['candle_begin_time'].iloc[-1]:
            print(f'utc时间:{_time}\t回测无该时间选币，跳过该时间。')
            compare_set.add(_time)
            continue

        # 整理实盘选币的symbol
        real_select_coin['symbol'] = real_select_coin['symbol'].map(lambda x: x.split('USDT')[0] + '-USDT')
        real_select_coin['symbol'] = real_select_coin['symbol'] + '(' + real_select_coin['symbol_type'] + ',' + \
                                     real_select_coin['方向'].astype(str) + ')'
        real_select_coin = set(real_select_coin['symbol'].values)

        # 整理回测选币的symbol
        _test_select_coin = test_select_coin[test_select_coin['candle_begin_time'] == _time]
        if _test_select_coin.empty:
            print(f'utc时间:{_time}\t回测无该时间选币，跳过该时间。')
            continue
        _test_select_coin['选币'] = _test_select_coin['选币'].map(ast.literal_eval)
        _test_select_coin = set(_test_select_coin['选币'].iloc[0])

        # 判断实盘和回测的选币是否相同
        if real_select_coin != _test_select_coin:
            s = f'utc时间:{_time}\t实盘回测选币有差异！\n'
            s += f'\t实盘选中但回测没有选中：{list(real_select_coin - _test_select_coin)}\n'
            s += f'\t回测选中但实盘没有选中：{list(_test_select_coin - real_select_coin)}\n'
            print(s)
            diff_time.add(_time)
        else:
            s = f'utc时间:{_time}\t实盘回测选币无差异'
            print(s)
            same_time.add(_time)

        # 更新exist_set和compare_time
        compare_set.add(_time)
        compare_time.add(_time)

        # 达到对比的周期数，即跳出，并打印出各个周期
        if len(compare_time) >= n:
            print(f'对比周期：{compare_time}')
            print(f'相同周期：{same_time}')
            print(f'不同周期：{diff_time}')
            break


def draw_equity_curve_plotly2(df, data_dict, date_col=None, right_axis=None, pic_size=[1500, 800], chg=False,
                             title=None, path='pic.html', show=True):
    """
    绘制策略曲线
    :param df: 包含净值数据的df
    :param data_dict: 要展示的数据字典格式：｛图片上显示的名字:df中的列名｝
    :param date_col: 时间列的名字，如果为None将用索引作为时间列
    :param right_axis: 右轴数据 ｛图片上显示的名字:df中的列名｝
    :param pic_size: 图片的尺寸
    :param chg: datadict中的数据是否为涨跌幅，True表示涨跌幅，False表示净值
    :param title: 标题
    :param path: 图片路径
    :param show: 是否打开图片
    :return:
    """
    draw_df = df.copy()

    # 设置时间序列
    if date_col:
        time_data = draw_df[date_col]
    else:
        time_data = draw_df.index

    # 绘制左轴数据
    fig = make_subplots(specs=[[{"secondary_y": True}]])
    for key in data_dict:
        if chg:
            draw_df[data_dict[key]] = (draw_df[data_dict[key]] + 1).fillna(1).cumprod()
        fig.add_trace(go.Scatter(x=time_data, y=draw_df[data_dict[key]], name=key, ))

    # 绘制右轴数据
    if right_axis:
        key = list(right_axis.keys())[0]
        fig.add_trace(go.Scatter(x=time_data, y=draw_df[right_axis[key]], name=key + '(右轴)',
                                 yaxis='y2'))  # 标明设置一个不同于trace1的一个坐标轴

    fig.update_layout(template="none", width=pic_size[0], height=pic_size[1], title_text=title,
                      hovermode="x unified", hoverlabel=dict(bgcolor='rgba(255,255,255,0.5)', ),
                      )
    fig.update_layout(
        updatemenus=[
            dict(
                buttons=[
                    dict(label="线性 y轴",
                         method="relayout",
                         args=[{"yaxis.type": "linear"}]),
                    dict(label="Log y轴",
                         method="relayout",
                         args=[{"yaxis.type": "log"}]),
                ])],
    )
    plot(figure_or_data=fig, filename=path, auto_open=False)

    fig.update_yaxes(
        showspikes=True, spikemode='across', spikesnap='cursor', spikedash='solid', spikethickness=1,  # 峰线
    )
    fig.update_xaxes(
        showspikes=True, spikemode='across+marker', spikesnap='cursor', spikedash='solid', spikethickness=1,  # 峰线
    )

    # 打开图片的html文件，需要判断系统的类型
    if show:
        res = os.system('start %s' % path)
        if res != 0:
            os.system('open %s' % path)


def get_transfer(exchange, transfer_path, utc_offset):
    """
    获取划转记录

    MAIN_UMFUTURE 现货钱包转向U本位合约钱包
    MAIN_MARGIN 现货钱包转向杠杆全仓钱包

    UMFUTURE_MAIN U本位合约钱包转向现货钱包
    UMFUTURE_MARGIN U本位合约钱包转向杠杆全仓钱包

    CMFUTURE_MAIN 币本位合约钱包转向现货钱包

    MARGIN_MAIN 杠杆全仓钱包转向现货钱包
    MARGIN_UMFUTURE 杠杆全仓钱包转向U本位合约钱包

    MAIN_FUNDING 现货钱包转向资金钱包
    FUNDING_MAIN 资金钱包转向现货钱包

    FUNDING_UMFUTURE 资金钱包转向U本位合约钱包
    UMFUTURE_FUNDING U本位合约钱包转向资金钱包

    MAIN_OPTION 现货钱包转向期权钱包
    OPTION_MAIN 期权钱包转向现货钱包

    UMFUTURE_OPTION U本位合约钱包转向期权钱包
    OPTION_UMFUTURE 期权钱包转向U本位合约钱包

    MAIN_PORTFOLIO_MARGIN 现货钱包转向统一账户钱包
    PORTFOLIO_MARGIN_MAIN 统一账户钱包转向现货钱包

    MAIN_ISOLATED_MARGIN 现货钱包转向逐仓账户钱包
    ISOLATED_MARGIN_MAIN 逐仓钱包转向现货账户钱包

    :param exchange:    交易所对象
    :param run_time:    运行时间
    """

    start_time = datetime.now().replace(minute=0, second=0, microsecond=0) - pd.Timedelta(days=180)
    add_type = ['CMFUTURE_MAIN', 'MARGIN_MAIN', 'MARGIN_UMFUTURE', 'FUNDING_MAIN', 'FUNDING_UMFUTURE',
                'OPTION_MAIN', 'OPTION_UMFUTURE', 'PORTFOLIO_MARGIN_MAIN', 'ISOLATED_MARGIN_MAIN']
    reduce_type = ['MAIN_MARGIN', 'UMFUTURE_MARGIN', 'MAIN_FUNDING', 'UMFUTURE_FUNDING', 'MAIN_OPTION',
                   'UMFUTURE_OPTION', 'MAIN_PORTFOLIO_MARGIN', 'MAIN_ISOLATED_MARGIN']

    result = []
    for _ in add_type + reduce_type:
        params = {
            'fromSymbol': 'USDT',
            'startTime': int(start_time.timestamp() * 1000),
            'type': _,
            'timestamp': int(round(time.time() * 1000)),
            'size': 100,
        }
        if _ == 'MAIN_ISOLATED_MARGIN':
            params['toSymbol'] = 'USDT'
            del params['fromSymbol']
        # 获取划转信息(取上一小时到当前时间的划转记录)
        try:
            # account_info = retry_wrapper(exchange.sapi_get_asset_transfer, params=params, func_name='获取万向划转历史', if_exit=False)
            account_info = exchange.sapi_get_asset_transfer(params)
        except:
            print(f'当前账户查询类型【{_}】失败，不影响后续操作，请忽略')
            continue
        if account_info and int(account_info['total']) > 0:
            res = pd.DataFrame(account_info['rows'])
            res['timestamp'] = pd.to_datetime(res['timestamp'], unit='ms')
            res.loc[res['type'].isin(add_type), 'flag'] = 1
            res.loc[res['type'].isin(reduce_type), 'flag'] = -1
            res = res[res['status'] == 'CONFIRMED']
            result.append(res)

    # 获取主账号与子账号之间划转记录
    result2 = []
    for transfer_type in [1, 2]:  # 1: 划入。从主账号划转进来  2: 划出。从子账号划转出去
        params = {
            'asset': 'USDT',
            'type': transfer_type,
            'startTime': int(start_time.timestamp() * 1000),
        }
        try:
            # account_info = retry_wrapper(exchange.sapi_get_sub_account_transfer_subuserhistory, params=params, func_name='获取账户间划转历史', if_exit=False)
            account_info = exchange.sapi_get_sub_account_transfer_subuserhistory(params)
        except:
            print(f'当前账户查询类型【{transfer_type}】失败，不影响后续操作，请忽略')
            continue
        if account_info and len(account_info):
            res = pd.DataFrame(account_info)
            res['time'] = pd.to_datetime(res['time'], unit='ms')
            res.rename(columns={'qty': 'amount', 'time': 'timestamp'}, inplace=True)
            res.loc[res['toAccountType'] == 'SPOT', 'flag'] = 1 if transfer_type == 1 else -1
            res.loc[res['toAccountType'] == 'FUTURE', 'flag'] = 1 if transfer_type == 1 else -1
            res = res[res['status'] == 'SUCCESS']
            res = res[res['toAccountType'].isin(['SPOT', 'FUTURE'])]
            result2.append(res)

    # 将账号之间的划转与单账号内部换转数据合并
    result.extend(result2)
    if not len(result):
        return pd.DataFrame()

    all_df = pd.concat(result, ignore_index=True)
    all_df.drop_duplicates(subset=['timestamp', 'tranId', 'flag'], inplace=True)
    all_df.sort_values('timestamp', inplace=True)

    all_df['amount'] = all_df['amount'].astype(float) * all_df['flag']
    all_df.rename(columns={'amount': '账户总净值'}, inplace=True)
    all_df['type'] = 'transfer'
    all_df = all_df[['timestamp', '账户总净值', 'type']]
    all_df['timestamp'] = all_df['timestamp'] + pd.Timedelta(hours=utc_offset)
    all_df.reset_index(inplace=True, drop=True)

    all_df['time'] = all_df['timestamp']
    result_df = all_df.resample(rule='1H', on='timestamp').agg({'time': 'last', '账户总净值': 'sum', 'type': 'last'})
    result_df = result_df[result_df['type'].notna()]
    result_df.reset_index(inplace=True, drop=True)

    if os.path.exists(transfer_path):
        exist_transfer_df = pd.read_csv(transfer_path, encoding='gbk', parse_dates=['time'])
        result_df = pd.concat([exist_transfer_df, result_df], axis=0)
        result_df = result_df.drop_duplicates(keep='first').reset_index(drop=True)
        result_df.to_csv(transfer_path, encoding='gbk', index=False)
    else:
        result_df.to_csv(transfer_path, encoding='gbk', index=False)

    return result_df


def curve_compare(exchange, real_curve, test_curve, data_path, account_name, utc_offset, start_time, end_time, root_path):
    """
    实盘和回测资金曲线对比
    :param exchange: 交易所对象
    :param real_curve: 实盘资金曲线
    :param test_curve: 回测资金曲线
    :param data_path: 实盘数据路径
    :param account_name: 账户名
    :param utc_offset: utc
    :param start_time: 资金曲线对比开始时间
    :param end_time: 资金曲线对比结束时间
    :param root_path: 回测数据路径
    :return:
        生成资金曲线对比图
    """
    print('=' * 20, '资金曲线对比', '=' * 20)

    # 读取实盘资金曲线文件
    if os.path.exists(real_curve):
        real_curve = pd.read_csv(real_curve, encoding='gbk', parse_dates=['time'])
    else:
        print(f'找不到实盘资金曲线文件，请检查文件及路径是否配置正确：{real_curve}')
        exit()
    real_curve['time'] = real_curve['time'].map(lambda x: x.replace(minute=0, second=0, microsecond=0))
    # =获取划转记录
    transfer_path = os.path.join(data_path, account_name, '账户信息', 'transfer.csv')
    transfer_df = get_transfer(exchange, transfer_path, utc_offset)

    # 处理划转资金导致的净值不匹配
    real_curve = deal_with_equity(real_curve, transfer_df)
    real_curve['涨跌幅'] = real_curve['净值'].pct_change()
    real_curve['time'] = real_curve['time'] - pd.Timedelta(hours=utc_offset + 1)
    # 截取开始时间后截止时间前的实盘资金曲线数据
    real_curve = real_curve[real_curve['time'] > start_time].reset_index(drop=True)
    if end_time:
        real_curve = real_curve[real_curve['time'] < end_time].reset_index(drop=True)
    # 计算资金曲线
    real_curve.rename(columns={'time': 'candle_begin_time', '涨跌幅': '实盘涨跌幅'}, inplace=True)
    real_curve = real_curve[['candle_begin_time', '实盘涨跌幅']]

    # 读取回测资金曲线文件
    if os.path.exists(test_curve):
        test_curve = pd.read_csv(test_curve, encoding='gbk', parse_dates=['candle_begin_time'])
    else:
        print(f'找不到回测资金曲线文件，请检查文件及路径是否配置正确：{test_curve}')
        exit()
    test_curve.rename(columns={'轮动涨跌幅': '涨跌幅'}, inplace=True)
    test_curve = test_curve[['candle_begin_time', '涨跌幅']]
    # 截取数据
    test_curve = test_curve[test_curve['candle_begin_time'] > start_time].reset_index(drop=True)
    if end_time:
        test_curve = test_curve[test_curve['candle_begin_time'] < end_time].reset_index(drop=True)
    # 计算资金曲线
    test_curve.rename(columns={'涨跌幅': '回测涨跌幅'}, inplace=True)

    # 合并实盘资金曲线和回测资金曲线
    curve = pd.merge(real_curve, test_curve, on='candle_begin_time', how='left')
    curve['实盘涨跌幅'].fillna(0, inplace=True)
    curve['回测涨跌幅'].fillna(0, inplace=True)
    curve['实盘资金曲线'] = (curve['实盘涨跌幅'] + 1).cumprod()
    curve['回测资金曲线'] = (curve['回测涨跌幅'] + 1).cumprod()
    curve['实盘-回测'] = curve['实盘资金曲线'] - curve['回测资金曲线']

    # 画图
    left_axis = {'回测资金曲线': '回测资金曲线', '实盘资金曲线': '实盘资金曲线'}
    right_axis = {'实盘-回测': '实盘-回测'}
    draw_equity_curve_plotly2(curve, left_axis, date_col='candle_begin_time', right_axis=right_axis,
                              path=root_path + '/data/pic3.html')


def factor_compare(inter_data, factor_data, factor_cols, start_time, hold_period):
    """
        对比轮动策略的因子值
    :param inter_data: 回测保存下来的中间数据，包含因子值数据
    :param factor_data: 实盘保存下来的因子值数据
    :param factor_cols: 因子列
    :param start_time: 对比因子值的开始时间
    :return:
    """
    print('=' * 20, '轮动因子值对比', '=' * 20)

    # 读取并整理回测因子值数据
    if os.path.exists(inter_data):
        inter_data = pd.read_pickle(inter_data)
    else:
        print(f'找不到轮动策略中间数据文件，请检查文件及路径是否配置正确：{inter_data}')
        exit()
    inter_data = inter_data[['candle_begin_time', 'symbol'] + factor_cols]

    # 读取并整理实盘因子值数据
    if os.path.exists(factor_data):
        factor_data = pd.read_csv(factor_data, encoding='gbk', parse_dates=['candle_begin_time'])
    else:
        print(f'找不到实盘因子值文件，请检查文件及路径是否配置正确：{factor_data}')
        exit()
    factor_data['symbol'] = factor_data['symbol'].map(
        lambda x: x.split('_eq_')[0].split('Strategy_')[1] + '_' + x.split('_eq_')[1].split('资金曲线')[0])
    factor_data = factor_data[['candle_begin_time', 'symbol'] + factor_cols]

    # 整理实盘因子值数据，只保留对应时间的因子值
    date_range = pd.DataFrame(pd.date_range(start=factor_data['candle_begin_time'].min(), end=factor_data['candle_begin_time'].max(), freq=hold_period))  # 创建2017-01-01至回测结束时间的1H列表
    date_range.rename(columns={0: 'candle_begin_time'}, inplace=True)
    factor_data = pd.merge(date_range, factor_data, on='candle_begin_time', how='left')
    factor_data = factor_data.sort_values(['candle_begin_time', 'symbol']).reset_index(drop=True)

    # 将实盘因子值和回测因子值合并一下，开始比对
    all_factor_data = pd.merge(inter_data, factor_data, on=['candle_begin_time', 'symbol'], how='right')
    all_factor_data = all_factor_data[all_factor_data['candle_begin_time'] > start_time].reset_index(drop=True)  # 只截取开始时间后的数据
    all_factor_data['因子值为空'] = all_factor_data.isnull().sum(axis=1)
    nan_factor_data = all_factor_data[all_factor_data['因子值为空'] == 1].reset_index(drop=True)
    all_factor_data = all_factor_data[all_factor_data['因子值为空'] == 0].reset_index(drop=True)
    nan_factor_data.drop('因子值为空', axis=1, inplace=True)
    all_factor_data.drop('因子值为空', axis=1, inplace=True)

    # 判断空因子值df是否为空，如果为空，则打印出来
    if not nan_factor_data.empty:
        for factor_col in factor_cols:
            nan_factor_data.rename(columns={f'{factor_col}_x': f'{factor_col}_回测', f'{factor_col}_y': f'{factor_col}_实盘'}, inplace=True)
            nan_factor_data[f'{factor_col}_回测'] = nan_factor_data[f'{factor_col}_回测'].round(6)
            nan_factor_data[f'{factor_col}_实盘'] = nan_factor_data[f'{factor_col}_实盘'].round(6)
        print(f'因子值为空的数据：\n{nan_factor_data}')

    # 除去空因子值后，是否还剩余有数据，如果有数据，则进行对比；如果没有数据，则退出
    if not all_factor_data.empty:
        diff_factor_data_list = []
        for factor_col in factor_cols:
            all_factor_data.rename(columns={f'{factor_col}_x': f'{factor_col}_回测', f'{factor_col}_y': f'{factor_col}_实盘'}, inplace=True)
            all_factor_data[f'{factor_col}_回测'] = all_factor_data[f'{factor_col}_回测'].round(6)
            all_factor_data[f'{factor_col}_实盘'] = all_factor_data[f'{factor_col}_实盘'].round(6)
            diff_factor_data = all_factor_data[all_factor_data[f'{factor_col}_回测'] != all_factor_data[f'{factor_col}_实盘']]
            diff_factor_data_list.append(diff_factor_data)
        diff_factor_data = pd.concat(diff_factor_data_list, axis=0)
        diff_factor_data = diff_factor_data.sort_values(['candle_begin_time', 'symbol']).reset_index(drop=True)
        # 判断是否一致
        if diff_factor_data.empty:
            print('回测与实盘完全一致！')
        else:
            print(f'回测和实盘存在不一致，不一致的数据为：\n{diff_factor_data}')
    else:
        print('所有因子值都为空，无法对比因子值，退出。')


def get_file_in_folder(path, file_type, contains=None, filters=[], drop_type=False):
    """
    获取指定文件夹下的文件
    :param path: 文件夹路径
    :param file_type: 文件类型
    :param contains: 需要包含的字符串，默认不含
    :param filters: 字符串中需要过滤掉的内容
    :param drop_type: 是否要保存文件类型
    :return:
    """
    file_list = os.listdir(path)
    file_list = [file for file in file_list if file_type in file]
    if contains:
        file_list = [file for file in file_list if contains in file]
    for con in filters:
        file_list = [file for file in file_list if con not in file]
    if drop_type:
        file_list = [file[:file.rfind('.')] for file in file_list]

    return file_list
